﻿using System;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using EnvDTE;
using Microsoft.Practices.Common.Services;
using Microsoft.Practices.RecipeFramework;
using Microsoft.Practices.RecipeFramework.Library.Actions;
using Microsoft.Practices.RecipeFramework.VisualStudio.Library.Templates;
using Microsoft.VisualStudio.TextTemplating;
using Sixeyed.CodeGeneration.Generation;
using Sixeyed.CodeGeneration.Metadata.Interfaces;

namespace Sixeyed.Guidance.CodeGeneration.Actions
{
    /// <summary>
    /// Generates project item(s) using a T4 template, and adds it to the selected project
    /// </summary>
    public class AddGeneratedItems : T4Action
    {
        private List<AddItemFromStringAction> _addItemActions;
        private List<AddItemFromStringAction> AddItemActions
        {
            get
            {
                if (this._addItemActions == null)
                {
                    this._addItemActions = new List<AddItemFromStringAction>();
                }
                return this._addItemActions;
            }
        }

        #region Input Properties

        /// <summary>
        /// Gets/sets configuration for the T4 template
        /// </summary>
        [Input(Required = true)]
        public TemplateConfiguration TemplateConfiguration { get; set; }

        /// <summary>
        /// Gets/sets the metadata items used to generate items
        /// </summary>
        /// <remarks>
        /// If there are multiple metadata items, the template will be run over each in turn
        /// </remarks>
        [Input(Required = true)]
        public ArrayList MetadataItems { get; set; }

        /// <summary>
        /// Project to add generated output
        /// </summary>
        [Input(Required = true)]
        public Project Project { get; set; }

        #endregion

        #region IAction Members

        /// <summary>
        /// Reads T4 template, sets up configuration and runs over each metadara item
        /// </summary>
        public override void Execute()
        {
            //read in the template:
            string templateCode = string.Empty;
            if (string.IsNullOrEmpty(this.TemplateConfiguration.TemplatePath))
            {
                throw new ArgumentNullException("TemplateConfiguration.TemplatePath");
            }
            try
            {
                templateCode = File.ReadAllText(this.TemplateConfiguration.TemplatePath);
            }
            catch (Exception ex)
            {
                throw new ArgumentException(string.Format("Error reading specified template: {0}, {1}", this.TemplateConfiguration.TemplatePath, ex.Message));
            }
            
            //default the target namespace:
            if (string.IsNullOrEmpty(this.TemplateConfiguration.TargetNamespace))
            {
                this.TemplateConfiguration.TargetNamespace = this.Project.Properties.Item("DefaultNamespace").Value.ToString();
            }

            //add custom arguments:
            if (!base.additionalArguments.Contains(ArgumentName.MetadataItem))
            {
                base.additionalArguments.Add(ArgumentName.MetadataItem, null);
            }
            if (!base.additionalArguments.Contains(ArgumentName.TemplateConfiguration))
            {
                base.additionalArguments.Add(ArgumentName.TemplateConfiguration, null);
            }
            base.additionalArguments[ArgumentName.TemplateConfiguration] = this.TemplateConfiguration.Clone();

            //if metadata is provided, loop through each item:   
            if (this.MetadataItems != null && this.MetadataItems.Count > 0)
            {
                foreach (IMetadataItem item in this.MetadataItems)
                {
                    base.additionalArguments[ArgumentName.MetadataItem] = item as IMetadataItem;
                    this.GenerateAndAddItem(templateCode);
                }
            }
            else
            {
                //otherwise, run the template once as is:
                this.GenerateAndAddItem(templateCode);
            }
        }
        
        /// <summary>
        /// Undoes effects of the action
        /// </summary>
        public override void Undo()
        {
            foreach (AddItemFromStringAction addAction in AddItemActions)
            {
                addAction.Undo();
            }
        }

        #endregion

        #region Private members

        /// <summary>
        /// Renders T4 template for the current set of arguments and adds output to project
        /// </summary>
        /// <param name="templateCode">T4 template text</param>
        private void GenerateAndAddItem(string templateCode)
        {
            string output = this.GetRenderedText(templateCode);

            //load config from template and set up any unspecified inputs:
            TemplateConfiguration templateConfig = base.additionalArguments[ArgumentName.TemplateConfiguration] as TemplateConfiguration;
            this.TemplateConfiguration.LoadFrom(templateConfig);

            //add the generated text as a project item:
            AddItemFromStringAction addAction = new AddItemFromStringAction();
            addAction.Site = this.Site;
            addAction.Content = output;
            addAction.TargetFileName = this.TemplateConfiguration.GetTargetFileName();
            addAction.Project = this.Project;
            addAction.Open = false;
            addAction.Execute();

            //store the action so we can undo:
            this.AddItemActions.Add(addAction);
        }

        /// <summary>
        /// Renders the T4 template 
        /// </summary>
        /// <remarks>
        /// Functionally similar to T4Action.Render
        /// </remarks>
        /// <param name="templateCode">T4 template string</param>
        /// <returns>Rendered text</returns>
        private string GetRenderedText(string templateCode)
        {
            string basePath = this.GetBasePath();
            ITextTemplatingEngine engine = new Engine();
            IValueInfoService service = (IValueInfoService)this.GetService(typeof(IValueInfoService));
            Dictionary<string, PropertyData> arguments = new Dictionary<string, PropertyData>();
            foreach (string argumentKey in base.additionalArguments.Keys)
            {
                Type type = null;
                try
                {
                    type = service.GetInfo(argumentKey).Type;
                }
                catch (ArgumentException)
                {
                    if (base.additionalArguments[argumentKey] == null)
                    {
                        continue;
                    }
                    type = base.additionalArguments[argumentKey].GetType();
                }
                PropertyData data = new PropertyData(base.additionalArguments[argumentKey], type);
                arguments.Add(argumentKey, data);
            }
            TemplateHost host = new TemplateHost(basePath, arguments);
            host.TemplateFile = this.TemplateConfiguration.TemplatePath;
            string generatedText = engine.ProcessTemplate(templateCode, host);
            if (host.Errors.HasErrors)
            {
                throw new TemplateException(host.Errors);
            }
            if (host.Errors.HasWarnings)
            {
                StringBuilder builder = new StringBuilder();
                foreach (CompilerError error in host.Errors)
                {
                    builder.AppendLine(error.ToString());
                }
                //Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, Resources.T4Action_CompilationWarnings, new object[] { templateFile, builder.ToString() }));
            }
            return generatedText;
        }

        private struct ArgumentName
        {
            public const string TemplateConfiguration = "TemplateConfiguration";
            public const string MetadataItem = "MetadataItem";
        }
        
        #endregion
    }
}